package cn.bossfriday.common.queue;

import cn.bossfriday.common.utils.ThreadPoolUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * BufferedQueue
 *
 * @author chenx
 */
public class BufferedQueue<T> {

    private final int maxCommitSize;
    private final long maxIntervalMillis;

    private final Consumer<List<T>> consumer;
    private final List<T> buffer;
    private final Object lock = new Object();

    private final ScheduledExecutorService scheduler;
    private final ExecutorService flushExecutor;

    public BufferedQueue(int maxCommitSize, long maxIntervalMillis, Consumer<List<T>> consumer) {
        this.maxCommitSize = maxCommitSize;
        this.maxIntervalMillis = maxIntervalMillis;
        this.consumer = consumer;
        this.buffer = new ArrayList<>();
        this.scheduler = Executors.newSingleThreadScheduledExecutor();
        this.flushExecutor = ThreadPoolUtil.getThreadPool("BufferedQueue-" + this.hashCode());

        this.scheduler.scheduleAtFixedRate(this::flushIfNeeded, this.maxIntervalMillis, this.maxIntervalMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * add
     *
     * @param item
     */
    public void add(T item) {
        synchronized (this.lock) {
            this.buffer.add(item);
            if (this.buffer.size() >= this.maxCommitSize) {
                this.flush();
            }
        }
    }

    /**
     * addAll
     *
     * @param items
     */
    public void addAll(List<T> items) {
        synchronized (this.lock) {
            this.buffer.addAll(items);
            if (this.buffer.size() >= this.maxCommitSize) {
                this.flush();
            }
        }
    }

    /**
     * shutdown
     */
    public void shutdown() {
        this.scheduler.shutdown();
        this.flushExecutor.shutdown();
        try {
            if (!this.scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                this.scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            this.scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    /**
     * flushIfNeeded
     */
    private void flushIfNeeded() {
        synchronized (this.lock) {
            if (!this.buffer.isEmpty()) {
                this.flush();
            }
        }
    }

    /**
     * flush
     */
    private void flush() {
        if (this.buffer.isEmpty()) {
            return;
        }

        List<T> toCommit = new ArrayList<>(this.buffer);
        this.buffer.clear();

        this.flushExecutor.submit(() -> this.consumer.accept(toCommit));
    }
}
